//
//  SFCTextFileDiff.m
//  SVNForCocoa
//
//  Created by Jeremy Pereira on 12/12/2013.
//  Copyright (c) 2013 Jeremy Pereira. All rights reserved.
//

#import "SFCTextFileDiff.h"
#import "SFCError.h"

@interface SFCDiffOutputStream : NSOutputStream <SFCLineGenerator>

@property (nonatomic, weak) id<SFCLineConsumer> consumer;
@property (nonatomic, readonly, assign) size_t lineNumber;
@property (nonatomic, strong) NSData* lineEnding;
@property (nonatomic, assign) bool isClosed;

-(void) appendToCurrentLine: (NSData*) lineChunk;
-(bool) consumeLineError: (NSError* __autoreleasing*) error;

@end

@interface SFCDiffFileParser ()

+(NSString*) makeErrorLine: (size_t) lineNumber
                    column: (size_t) column
                      text: (NSString*) text;

@end

@interface SFCTextFileDiff ()

-(void) calculateHunkNumbers;


@end

@interface SFCDiffHunk ()

-(id) initWithOwner: (SFCTextFileDiff*) owner;

-(bool) parseHeaderLine: (NSString*) line
		  fromGenerator: (id<SFCLineGenerator>) generator
                  error: (NSError* __autoreleasing*) error;
+(NSRange) makeRangeFromString: (NSString*) aString;
+(NSNumberFormatter*) locationFormatter;
+(NSNumberFormatter*) lengthFormatter;

-(void) calculateLineNumbers;

@property (nonatomic, assign) size_t sequenceNumber;

@end

@interface SFCDiffLine ()

-(id) initWithType: (SFCDiffType) diffType text: (NSString*) text;

@property (nonatomic, assign) NSInteger leftLineNumber;
@property (nonatomic, assign) NSInteger rightLineNumber;

@end

@implementation SFCDiffFileParser

@synthesize owner = _owner;

-(id) init
{
    return [self initWithOwner: nil];
}

-(id) initWithOwner: (id<SFCLineConsumer>) owner
{
    self = [super init];
    if (self != nil)
    {
        _owner = owner;
    }
    return self;
}

-(NSUInteger) countByEnumeratingWithState: (NSFastEnumerationState*) state
                                  objects: (__unsafe_unretained id []) buffer
                                    count:(NSUInteger)len
{
	@throw [NSException exceptionWithName: NSGenericException
                                   reason: [NSString stringWithFormat: @"%@ needs to override %s", [self className], __PRETTY_FUNCTION__]
                                 userInfo: nil];
}

-(bool) generator: (id<SFCLineGenerator>) generator
      consumeLine: (NSString*) line
       isConsumed: (bool*) isConsumed
            error: (NSError *__autoreleasing *) error
{
	@throw [NSException exceptionWithName: NSGenericException
                                   reason: [NSString stringWithFormat: @"%@ needs to override %s", [self className], __PRETTY_FUNCTION__]
                                 userInfo: nil];
}

-(bool) generator: (id<SFCLineGenerator>) generator
consumeEOFisConsumed:(bool *)isConsumed
            error:(NSError *__autoreleasing *)error
{
    [generator setConsumer: [self owner]];
    *isConsumed = [self owner] == nil;
    return true;
}

+(NSString*) makeErrorLine: (size_t) lineNumber
                    column: (size_t) column
                      text: (NSString*) text
{
	return [NSString stringWithFormat: @"%lu:%lu:%@", lineNumber, column, text];
}

@end

@implementation SFCDiffCollection
{
    NSMutableArray* diffs;
    SFCDiffOutputStream* _outputStream;
}

-(id) initWithOwner: (id<SFCLineConsumer>)owner
{
	self = [super initWithOwner: owner];
    if (self != nil)
    {
        diffs = [[NSMutableArray alloc] init];
    }
    return self;
}

-(NSUInteger) countByEnumeratingWithState: (NSFastEnumerationState*) state
                                  objects: (__unsafe_unretained id []) buffer
                                    count:(NSUInteger)len
{
	return [diffs countByEnumeratingWithState: state objects: buffer count: len];
}

-(NSOutputStream*) outputStream
{
    if (_outputStream == nil)
    {
		_outputStream = [[SFCDiffOutputStream alloc] init];
        [_outputStream setConsumer: self];
    }
    return _outputStream;
}

-(size_t) diffCount
{
    return [diffs count];
}

-(SFCDiffHunk*) diffAtIndex: (size_t)index
{
    return [diffs objectAtIndex: index];
}

-(bool) generator: (id<SFCLineGenerator>) generator
      consumeLine: (NSString*) line
       isConsumed: (bool*) isConsumed
            error: (NSError *__autoreleasing *) error
{
    bool ret = false;
    if ([line hasPrefix: @"Index:"])
    {
        SFCTextFileDiff* newDiff = [[SFCTextFileDiff alloc] initWithOwner: self];
        [generator setConsumer: newDiff];
        [diffs addObject: newDiff];
        *isConsumed = false;
        ret = true;
    }
    else if ([line hasPrefix: @"@@"])
    {
        /*
         *  The stream goes straight into hunks
         */
        SFCTextFileDiff* newDiff = [[SFCTextFileDiff alloc] initWithOwner: self];
        [generator setConsumer: newDiff];
        [diffs addObject: newDiff];
        *isConsumed = false;
        ret = true;
    }
    else
    {
        NSLog(@"Parse error with line \"%@\"", line);
		if (error != nil)
        {
            *error = [SFCError errorWithDomain: SFC_ERROR_DOMAIN
                                          code: SFC_ERROR_DIFF_PARSE
                                      userInfo: @{ NSLocalizedDescriptionKey: [[self class] makeErrorLine: [generator lineNumber]
                                                                                                   column: 1
                                                                                                     text: NSLocalizedString(@"Expected a diff line, invalid prefix", @"")],
                                                   @"net.jeremyp.sfc.diffline" : line}];
        }
        *isConsumed = true;
    }
    return ret;
}

@end

@implementation SFCTextFileDiff
{
    NSMutableArray* hunks;
}

@synthesize fileName = _fileName;

-(id) initWithOwner: (id<SFCLineConsumer>) owner
{
    self = [super initWithOwner: owner];
    if (self != nil)
    {
        hunks = [[NSMutableArray alloc] init];
    }
    return self;
}

-(NSUInteger) countByEnumeratingWithState: (NSFastEnumerationState*) state
                                  objects: (__unsafe_unretained id []) buffer
                                    count:(NSUInteger)len
{
	return [hunks countByEnumeratingWithState: state objects: buffer count: len];
}

-(size_t) hunkCount
{
    return [hunks count];
}

-(SFCDiffHunk*) hunkAtIndex: (size_t)index
{
    return [hunks objectAtIndex: index];
}

-(bool) generator: (id<SFCLineGenerator>) generator
      consumeLine: (NSString*) line
       isConsumed: (bool*) isConsumed
            error: (NSError *__autoreleasing *) error
{
    bool ret = false;
    if ([line hasPrefix: @"@@"])
    {
		SFCDiffHunk* hunk = [[SFCDiffHunk alloc] initWithOwner: self];
        [generator setConsumer: hunk];
        [hunks addObject: hunk];
        *isConsumed = false;
        ret = true;
    }
    else if ([line hasPrefix: @"Index:"] && [self fileName] == nil)
    {
		NSString* fileName = [line substringFromIndex: [@"Index: " length]];
        [self setFileName: fileName];
        ret = true;
        *isConsumed = true;
    }
    else if ([line hasPrefix: @"Index:"])
	{
        /*
         *  This line marks the start of the next file.
         */
        [self calculateHunkNumbers];
        *isConsumed = false;
        [generator setConsumer: [self owner]];
        ret = true;
    }
    else if ([line hasPrefix: @"=="]
             || [line hasPrefix: @"---"]
             || [line hasPrefix: @"+++"])
    {
        NSLog(@"TODO: Do not ignore %@", line);
        ret = true;
        *isConsumed = true;
    }
    else
    {
        NSLog(@"Parse error with line \"%@\"", line);
		if (error != nil)
        {
            *error = [SFCError errorWithDomain: SFC_ERROR_DOMAIN
                                          code: SFC_ERROR_DIFF_PARSE
                                      userInfo: @{ NSLocalizedDescriptionKey: [[self class] makeErrorLine: [generator lineNumber]
                                                                                                   column: 1
                                                                                                     text: NSLocalizedString(@"Expected a diff line, invalid prefix", @"")],
                                                   @"net.jeremyp.sfc.diffline" : line}];
        }
        *isConsumed = true;
    }
    return ret;
}

-(bool) generator: (id<SFCLineGenerator>) generator
consumeEOFisConsumed:(bool *)isConsumed
            error:(NSError *__autoreleasing *)error
{
	[self calculateHunkNumbers];
    return [super generator: generator
       consumeEOFisConsumed: isConsumed
                      error: error];
}

-(void) calculateHunkNumbers
{
    size_t hunkNumber = 0;
    for (SFCDiffHunk* hunk in self)
    {
        [hunk setSequenceNumber: ++hunkNumber];
    }
}

@end

@implementation SFCDiffOutputStream
{
    NSMutableData* currentLine;
    NSError* streamError;
}

@synthesize consumer = _consumer;
@synthesize lineNumber = _lineNumber;
@synthesize lineEnding = _lineEnding;
@synthesize isClosed = _isClosed;

-(id) init
{
    self = [super init];
    if (self != nil)
    {
        _lineEnding = [@"\n" dataUsingEncoding: NSUTF8StringEncoding];
    }
    return self;
}

-(void) close
{
    if ([self isClosed])
    {
        @throw [NSException exceptionWithName: [self className]
                                       reason: @"Trying to close stream that is closed"
                                     userInfo: nil];
    }
    [self setIsClosed: true];
    bool isConsumed = false;
    bool isOK = true;
    while (isOK && !isConsumed && [self consumer] != nil)
    {
        isOK = [[self consumer] generator: self
              		 consumeEOFisConsumed: &isConsumed
                     		        error: nil];
    }
}

-(NSError*) streamError
{
	return streamError;
}

-(NSInteger) write: (const uint8_t*) buffer maxLength: (NSUInteger)len
{
    NSData* theNewData = [NSData dataWithBytes: buffer length: len];
    NSRange rangeLeft = NSMakeRange(0, len);
    bool isOk = true;
    while (rangeLeft.length > 0 && isOk)
    {
        NSRange endOfLine = [theNewData rangeOfData: [self lineEnding]
                                            options: 0
                                              range: rangeLeft];
        if (endOfLine.location == NSNotFound)
        {
			[self appendToCurrentLine: [theNewData subdataWithRange: rangeLeft]];
            rangeLeft.location = len;
            rangeLeft.length = 0;
        }
        else
        {
            NSRange chunkRange = NSMakeRange(rangeLeft.location,
                                             endOfLine.location - rangeLeft.location);
            [self appendToCurrentLine:[theNewData subdataWithRange: chunkRange]];
            NSError* localError = nil;
            isOk = [self consumeLineError: &localError];
            if (isOk)
            {
                NSUInteger bytesConsumed = endOfLine.location + endOfLine.length;
                rangeLeft.location = bytesConsumed;
                rangeLeft.length = len - bytesConsumed;
            }
            else
            {
                streamError = localError;
            }
        }
    }
	return isOk ? len : -1;
}

-(void) appendToCurrentLine: (NSData*) lineChunk
{
    if (currentLine == nil)
    {
        currentLine = [lineChunk mutableCopy];
    }
    else
    {
        [currentLine appendData: lineChunk];
    }
}

-(bool) consumeLineError: (NSError* __autoreleasing*) error
{
    NSString* theLine = [[NSString alloc] initWithData: currentLine
                                              encoding: NSUTF8StringEncoding];
    currentLine = nil;
    _lineNumber++;
    bool isConsumed = false;
    bool isOk = true;
    /*
     *  We musn't try to invoke the consumer if it is nil as it will cause us to
     *  return false without setting an error.  If the consumer is nil, we just
     *  eat the line.
     *
     *  The existing consumer is allowed to change the consumer to something
     *  else.  We need to check it each iteration.
     */
    while (isOk && !isConsumed && [self consumer] != nil)
    {
        isOk = [[self consumer] generator: self
                              consumeLine: theLine
                               isConsumed: &isConsumed
                                    error: error];
    }
    return isOk;
}

-(BOOL) hasSpaceAvailable
{
    return YES;
}

@end

@implementation SFCDiffHunk
{
    NSMutableArray* lines;
}

@synthesize leftRange = _leftRange;
@synthesize rightRange = _rightRange;
@synthesize sequenceNumber = _sequenceNumber;

-(id) initWithOwner: (SFCTextFileDiff*) owner
{
    self = [super initWithOwner: owner];
    if (self != nil)
    {
        _leftRange.location = NSNotFound;
        _rightRange.location = NSNotFound;
        lines = [[NSMutableArray alloc] init];
    }
    return self;
}

-(size_t) lineCount
{
    return [lines count];
}

-(SFCDiffLine*) lineAtIndex: (size_t) index
{
    return [lines objectAtIndex: index];
}

-(NSUInteger) countByEnumeratingWithState: (NSFastEnumerationState*) state
                                  objects: (__unsafe_unretained id [])buffer
                                    count: (NSUInteger) len
{
    return [lines countByEnumeratingWithState: state objects: buffer count: len];
}

-(bool) generator: (id<SFCLineGenerator>) generator
      consumeLine: (NSString*) line
       isConsumed: (bool*) isConsumed
            error: (NSError* __autoreleasing*) error
{
    *isConsumed = true;
    bool ret = true;

    if ([line hasPrefix: @"@@"] && [self leftRange].location == NSNotFound)
    {
        /*
         *  If we already have the ranges, we are at the hunk header of the next
         *  hunk.  If not, this is the start of this hunk.
         */
        ret = [self parseHeaderLine: line
                      fromGenerator: generator
                              error: error];
    }
    else if ([line hasPrefix: @"@@"] || [line hasPrefix: @"Index:"])
    {
        [self calculateLineNumbers];
        *isConsumed = false;
        [generator setConsumer: [self owner]];
    }
    else if ([line hasPrefix: @"+"] || [line hasPrefix: @"-"] || [line hasPrefix: @" "])
    {
		SFCDiffLine* theDiffLine = [[SFCDiffLine alloc] initWithType: [line characterAtIndex: 0]
                                                                text: [line substringFromIndex: 1]];
        [lines addObject: theDiffLine];
    }
    else
    {
        NSLog(@"Parse error with line \"%@\"", line);
		if (error != nil)
        {
            *error = [SFCError errorWithDomain: SFC_ERROR_DOMAIN
                                          code: SFC_ERROR_DIFF_PARSE
                                      userInfo: @{ NSLocalizedDescriptionKey:
                                                       [[SFCTextFileDiff class] makeErrorLine: [generator lineNumber]
                                                                                       column: 1
                                                                                         text: NSLocalizedString(@"Expected a diff line, invalid prefix", @"")],
                                                   @"net.jeremyp.sfc.diffline" : line}];
         }
        *isConsumed = true;
    }
    return ret;
}

-(bool) generator: (id<SFCLineGenerator>) generator
consumeEOFisConsumed:(bool *)isConsumed
            error:(NSError *__autoreleasing *)error
{
    [self calculateLineNumbers];
    return [super generator: generator
       consumeEOFisConsumed: isConsumed
       	              error: error];
}


-(void) calculateLineNumbers
{
    NSInteger leftLineNumber = [self leftRange].location;
    NSInteger rightLineNumber = [self rightRange].location;

    for (SFCDiffLine* line in self)
    {
        switch ([line diffType])
        {
            case SFC_DIFF_IDENTITY:
                [line setLeftLineNumber: leftLineNumber];
                [line setRightLineNumber: rightLineNumber];
                leftLineNumber++;
                rightLineNumber++;
                break;
            case SFC_DIFF_ADDITION:
                [line setLeftLineNumber: NSNotFound];
                [line setRightLineNumber: rightLineNumber];
                rightLineNumber++;
                break;
            case SFC_DIFF_DELETION:
                [line setLeftLineNumber: leftLineNumber];
                [line setRightLineNumber: NSNotFound];
                leftLineNumber++;
                break;

            default:
                break;
        }
    }
}


+(NSNumberFormatter*) locationFormatter
{
    static NSNumberFormatter* ret = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken,
    ^{
        ret = [[NSNumberFormatter alloc] init];
        [ret setAllowsFloats: NO];
        [ret setPositivePrefix: @"+"];
    });
    return ret;
}


+(NSNumberFormatter*) lengthFormatter
{
    static NSNumberFormatter* ret = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken,
                  ^{
                      ret = [[NSNumberFormatter alloc] init];
                      [ret setAllowsFloats: NO];
                      [ret setMinimum: @(0)];
                  });
    return ret;
}


+(NSRange) makeRangeFromString: (NSString*) aString
{
	NSRange ret = { NSNotFound, 0 };

    NSArray* rangeParts = [aString componentsSeparatedByString: @","];
    if ([rangeParts count] == 2)
    {
        NSNumber* location = [[[self class] locationFormatter] numberFromString: [rangeParts objectAtIndex: 0]];
		NSNumber* size = [[[self class] lengthFormatter] numberFromString: [rangeParts objectAtIndex: 1]];
		int locationAsInt = [location intValue];
        int sizeAsInt = [size intValue];
        if (locationAsInt < 0)
        {
            locationAsInt = -locationAsInt;
        }
        ret.location = locationAsInt;
        ret.length = sizeAsInt;
    }
    return ret;
}

-(bool) parseHeaderLine: (NSString*) line
          fromGenerator: (id<SFCLineGenerator>) generator
                  error: (NSError* __autoreleasing*) error
{
    bool ret = false;
    NSArray* headerParts = [line componentsSeparatedByString: @" "];
    /*
     *  Should have @[ @"@@", leftRange, rightRange, @"@@"]
     */
	if ([headerParts count] == 4)
    {
		[self setLeftRange: [[self class] makeRangeFromString: [headerParts objectAtIndex: 1]]];
		[self setRightRange: [[self class] makeRangeFromString: [headerParts objectAtIndex: 2]]];
		ret = [self leftRange].location != NSNotFound
        		&& [self rightRange].location != NSNotFound;
    }
    if (!ret && error != nil)
    {
		*error = [SFCError errorWithDomain: SFC_ERROR_DOMAIN
                                      code: SFC_ERROR_DIFF_PARSE
                      localizedDescription: [SFCTextFileDiff makeErrorLine: [generator lineNumber]
                                                                    column: 1
                                                                      text: NSLocalizedString(@"Header cannot be parsed", @"")]];
    }
    return ret;
}

-(NSString*) header
{
    return [NSString stringWithFormat:
            @"@@ -%ld,%ld +%ld,%ld @@",
            (long)[self leftRange].location,
            (long)[self leftRange].length,
            (long)[self rightRange].location,
            (long)[self rightRange].length];
}

@end

@implementation SFCDiffLine

@synthesize diffType = _diffType;
@synthesize text = _text;
@synthesize leftLineNumber = _leftLineNumber;
@synthesize rightLineNumber = _rightLineNumber;

-(id) initWithType: (SFCDiffType) diffType text: (NSString*) text
{
    self = [super init];
    if (self != nil)
    {
        _text = text;
        _diffType = diffType;
        _leftLineNumber = NSNotFound;
        _rightLineNumber = NSNotFound;
    }
    return self;
}

@end
